<?php
/**
 * Allows inline editing of grid field records without having to load a separate
 * edit interface.
 *
 * The form fields used can be configured by setting the value in {@link setDisplayFields()} to one
 * of the following forms:
 *   - A Closure which returns the field instance.
 *   - An array with a `callback` key pointing to a function which returns the field.
 *   - An array with a `field` key->response specifying the field class to use.
 */
class GridFieldEditableColumns extends GridFieldDataColumns implements
	GridField_HTMLProvider,
	GridField_SaveHandler,
	GridField_URLHandler {

	private static $allowed_actions = array(
		'handleForm'
	);

	/**
	 * @var Form[]
	 */
	protected $forms = array();

	public function getColumnContent($grid, $record, $col) {
		if(!$record->canEdit()) {
			return parent::getColumnContent($grid, $record, $col);
		}

		$fields = $this->getForm($grid, $record)->Fields();
		$value  = $grid->getDataFieldValue($record, $col);
		$rel = (strpos($col,'.') === false); // field references a relation value
		$field = ($rel) ? clone $fields->fieldByName($col) : new ReadonlyField($col);

		if(!$field) {
			throw new Exception("Could not find the field '$col'");
		}

		if(array_key_exists($col, $this->fieldCasting)) {
			$value = $grid->getCastedValue($value, $this->fieldCasting[$col]);
		}

		$value = $this->formatValue($grid, $record, $col, $value);

		$field->setName($this->getFieldName($field->getName(), $grid, $record));
		$field->setValue($value);

		return $field->forTemplate();
	}

	public function getHTMLFragments($grid) {
		GridFieldExtensions::include_requirements();
		$grid->addExtraClass('ss-gridfield-editable');
	}

	public function handleSave(GridField $grid, DataObjectInterface $record) {
		$list  = $grid->getList();
		$value = $grid->Value();

		if(!isset($value[__CLASS__]) || !is_array($value[__CLASS__])) {
			return;
		}

		$form = $this->getForm($grid, $record);

		foreach($value[__CLASS__] as $id => $fields) {
			if(!is_numeric($id) || !is_array($fields)) {
				continue;
			}

			$item = $list->byID($id);

			if(!$item || !$item->canEdit()) {
				continue;
			}

			$extra = array();

			$form->loadDataFrom($fields, Form::MERGE_CLEAR_MISSING);
			$form->saveInto($item);

			if($list instanceof ManyManyList) {
				$extra = array_intersect_key($form->getData(), (array) $list->getExtraFields());
			}

			$item->write();
			$list->add($item, $extra);
		}
	}

	public function handleForm(GridField $grid, $request) {
		$id   = $request->param('ID');
		$list = $grid->getList();

		if(!ctype_digit($id)) {
			throw new SS_HTTPResponse_Exception(null, 400);
		}

		if(!$record = $list->byID($id)) {
			throw new SS_HTTPResponse_Exception(null, 404);
		}

		$form = $this->getForm($grid, $record);

		foreach($form->Fields() as $field) {
			$field->setName($this->getFieldName($field->getName(), $grid, $record));
		}

		return $form;
	}

	public function getURLHandlers($grid) {
		return array(
			'editable/form/$ID' => 'handleForm'
		);
	}

	/**
	 * Gets the field list for a record.
	 *
	 * @param GridField $grid
	 * @param DataObjectInterface $record
	 * @return FieldList
	 */
	public function getFields(GridField $grid, DataObjectInterface $record) {
		$cols   = $this->getDisplayFields($grid);
		$fields = new FieldList();

		$list   = $grid->getList();
		$class  = $list ? $list->dataClass() : null;

		foreach($cols as $col => $info) {
			$field = null;

			if($info instanceof Closure) {
				$field = call_user_func($info, $record, $col, $grid);
			} elseif(is_array($info)) {
				if(isset($info['callback'])) {
					$field = call_user_func($info['callback'], $record, $col, $grid);
				} elseif(isset($info['field'])) {
					if ($info['field'] == 'LiteralField') {
						$field = new $info['field']($col, null);
					}else{
						$field = new $info['field']($col);
					}
				}

				if(!$field instanceof FormField) {
					throw new Exception(sprintf(
						'The field for column "%s" is not a valid form field',
						$col
					));
				}
			}

			if(!$field && $list instanceof ManyManyList) {
				$extra = $list->getExtraFields();

				if($extra && array_key_exists($col, $extra)) {
					$field = Object::create_from_string($extra[$col], $col)->scaffoldFormField();
				}
			}

			if(!$field) {
				if($class && $obj = singleton($class)->dbObject($col)) {
					$field = $obj->scaffoldFormField();
				} else {
					$field = new ReadonlyField($col);
				}
			}

			if(!$field instanceof FormField) {
				throw new Exception(sprintf(
					'Invalid form field instance for column "%s"', $col
				));
			}

			$fields->push($field);
		}

		return $fields;
	}

	/**
	 * Gets the form instance for a record.
	 *
	 * @param GridField $grid
	 * @param DataObjectInterface $record
	 * @return Form
	 */
	public function getForm(GridField $grid, DataObjectInterface $record) {
		$fields = $this->getFields($grid, $record);

		$form = new Form($this, null, $fields, new FieldList());
		$form->loadDataFrom($record);

		$form->setFormAction(Controller::join_links(
			$grid->Link(), 'editable/form', $record->ID
		));

		return $form;
	}

	protected function getFieldName($name,  GridField $grid, DataObjectInterface $record) {
		return sprintf(
			'%s[%s][%s][%s]', $grid->getName(), __CLASS__, $record->ID, $name
		);
	}

}
